import sys, os
from paraview import servermanager
import SOAPpy
from SOAPpy import WSDL
from threading import Thread
import time
import socket
from math import sqrt, acos, cos, sin, ceil
from paraview import vtk
import Image, ImageEnhance

sys.path.append('/home/jeroen/zoomify/ZoomifyImage')
from ZoomifyFileProcessor2 import ZoomifyFileProcessor

class Watermark:
	# the code below was taken from http://code.activestate.com/recipes/362879/
	def reduce_opacity(im, opacity):
		"""Returns an image with reduced opacity."""
		assert opacity >= 0 and opacity <= 1
		if im.mode != 'RGBA':
			im = im.convert('RGBA')
		else:
			im = im.copy()
		alpha = im.split()[3]
		alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
		im.putalpha(alpha)
		return im
	
	def watermark(im, mark, position, opacity=1):
		"""Adds a watermark to an image."""
		if opacity < 1:
			mark = Watermark.reduce_opacity(mark, opacity)
		if im.mode != 'RGBA':
			im = im.convert('RGBA')
	    # create a transparent layer the size of the image and draw the
	    # watermark in that layer.
		layer = Image.new('RGBA', im.size, (0,0,0,0))
		if position == 'tile':
			for y in range(0, im.size[1], mark.size[1]):
				for x in range(0, im.size[0], mark.size[0]):
					layer.paste(mark, (x, y))
		elif position == 'scale':
			# scale, but preserve the aspect ratio
			ratio = min(float(im.size[0]) / mark.size[0], float(im.size[1]) / mark.size[1])
			w = int(mark.size[0] * ratio)
			h = int(mark.size[1] * ratio)
			mark = mark.resize((w, h))
			layer.paste(mark, ((im.size[0] - w) / 2, (im.size[1] - h) / 2))
		else:
			layer.paste(mark, position)
		# composite the watermark with the layer
		return Image.composite(layer, im, layer)
	
	def watermark_image(sourceFile, watermarkFile, targetFile):
		im = Image.open(sourceFile)
		mark = Image.open(watermarkFile)
		watermarkedImage = Watermark.watermark(im, mark, 'tile', 0.5)
		watermarkedImage.save(targetFile)
		# watermark(im, mark, 'scale', 1.0).show()
		# watermark(im, mark, (100, 100), 0.5).show()

	watermark_image = staticmethod(watermark_image)
	watermark = staticmethod(watermark)
	reduce_opacity = staticmethod(reduce_opacity)

# Utility class for vector calculations
class Vector:
	def Length( vector ):
		return sqrt(vector[0]*vector[0]+vector[1]*vector[1]+vector[2]*vector[2])

	def Normalize( vec ):
		length = Vector.Length(vec)
		if length == 0:
			return vec
		vec[0] = vec[0] / length
		vec[1] = vec[1] / length
		vec[2] = vec[2] / length
		
		return vec

	def Cross( vec1, vec2 ):
		return [( vec1[1] * vec2[2] ) - ( vec1[2] * vec2[1] ), 
	  			( vec1[2] * vec2[0] ) - ( vec1[0] * vec2[2] ), 
	  			( vec1[0] * vec2[1] ) - ( vec1[1] * vec2[0] ) ]
   
	def Dot( vec1, vec2 ):
		return vec1[0] * vec2[0] + vec1[1] * vec2[1] + vec1[2] * vec2[2]

	def FindLeastParallelAxis( vec ):
		# fixed: without the abs() below the least parallel axis of a normal vector [0, 0, -1] would end up to be [0, 0, 1],
		# which is obviously not correct
		dotX = abs(Vector.Dot(vec, [1, 0, 0]))
		dotY = abs(Vector.Dot(vec, [0, 1, 0]))
		dotZ = abs(Vector.Dot(vec, [0, 0, 1]))
		
		if dotX <= dotY:
			if dotX <= dotZ:
				return [1, 0, 0]
			elif dotY <= dotZ:
				return [0, 1, 0]
			
		return [0, 0, 1]
   
	def Substract( vec1, vec2 ):
		return [ vec1[0] - vec2[0], vec1[1] - vec2[1], vec1[2] - vec2[2] ]

	def Add( vec1, vec2 ):
		return [vec1[0]+vec2[0], vec1[1]+vec2[1], vec1[2]+vec2[2]]

	def Multiply( vec, scalar ):
		return [vec[0] * scalar, vec[1] * scalar, vec[2] * scalar ]

	def ProjectOntoPlane( vec, normal ):
		# normalize normal
		# Vector.Normalize(normal) # this function is called from GetDirectionCosines in which the normal has already been normalized
		
		# projected = V - (V . Normal)*N
		return Vector.Substract( vec, Vector.Multiply( normal, Vector.Dot( vec,  normal ) ) )

	def MultiplyVectors(vec1, vec2):
		return vec1[0] * vec2[0] + vec1[1] * vec2[1] + vec1[2] * vec2[2]
	
	def GetEyeVector(center, normal):
		return[center[0]+normal[0]*center[0], center[1]+normal[1]*center[1], center[2]+normal[2]*center[2]];
	
	def GetDirectionCosines( normal ):
		Vector.Normalize( normal )
		leastParallelAxis = Vector.FindLeastParallelAxis( normal )
		projectedXAxis = Vector.ProjectOntoPlane( leastParallelAxis, normal )
		Vector.Normalize(projectedXAxis)
		projectedYAxis = Vector.Cross( projectedXAxis, normal )
		Vector.Normalize(projectedYAxis)
		
		return [projectedXAxis[0], projectedXAxis[1], projectedXAxis[2],
				projectedYAxis[0], projectedYAxis[1], projectedYAxis[2],
				normal[0], normal[1], normal[2]]

	# this function returns a transformation matrix to transform points in 3D to a 2D plane, 
	# using a direction cosines matrix and a plane origin
	def Calculate3DTo2DPlaneTransformation( cutplaneDirectionCosines, cutplaneOrigin ): 
		resliceAxesMatrix = vtk.vtkMatrix4x4()
		# the vectors (rows) in the direction cosines matrix are columns in the transformation matrix
		resliceAxesMatrix.DeepCopy([cutplaneDirectionCosines[0], cutplaneDirectionCosines[3],cutplaneDirectionCosines[6], cutplaneOrigin[0], cutplaneDirectionCosines[1],cutplaneDirectionCosines[4],cutplaneDirectionCosines[7], cutplaneOrigin[1], cutplaneDirectionCosines[2], cutplaneDirectionCosines[5], cutplaneDirectionCosines[8], cutplaneOrigin[2], 0, 0, 0, 1])
		invertedResliceAxesMatrix = vtk.vtkMatrix4x4()
		vtk.vtkMatrix4x4.Invert(resliceAxesMatrix, invertedResliceAxesMatrix)
		transformationMatrix = [invertedResliceAxesMatrix.GetElement(0,0),invertedResliceAxesMatrix.GetElement(0,1),invertedResliceAxesMatrix.GetElement(0,2),invertedResliceAxesMatrix.GetElement(0,3),invertedResliceAxesMatrix.GetElement(1,0),invertedResliceAxesMatrix.GetElement(1,1),invertedResliceAxesMatrix.GetElement(1,2),invertedResliceAxesMatrix.GetElement(1,3),invertedResliceAxesMatrix.GetElement(2,0),invertedResliceAxesMatrix.GetElement(2,1),invertedResliceAxesMatrix.GetElement(2, 2),invertedResliceAxesMatrix.GetElement(2,3),invertedResliceAxesMatrix.GetElement(3, 0),invertedResliceAxesMatrix.GetElement(3,1),invertedResliceAxesMatrix.GetElement(3,2),invertedResliceAxesMatrix.GetElement(3,3)] 
	
		return transformationMatrix

	Calculate3DTo2DPlaneTransformation = staticmethod(Calculate3DTo2DPlaneTransformation)
	GetDirectionCosines = staticmethod(GetDirectionCosines)
	FindLeastParallelAxis = staticmethod(FindLeastParallelAxis)
	Dot = staticmethod(Dot)
	Normalize = staticmethod(Normalize)
	ProjectOntoPlane = staticmethod(ProjectOntoPlane)
	Multiply = staticmethod(Multiply)
	Substract = staticmethod(Substract)
	Cross = staticmethod(Cross)
	Length = staticmethod(Length)
	GetEyeVector = staticmethod(GetEyeVector)
	MultiplyVectors = staticmethod(MultiplyVectors)
	


	
# heartbeat object to check whether the server is still alive	
class Heartbeat(Thread):
	__serverId = None

	def run(self):
		while 1:
			time.sleep(10)
			try:
				# get the server id by doing a ping() 
				id = proxy.ping()
				if self.__serverId == None:
					self.__serverId = id
				elif self.__serverId != id:
					print "Detected different serverId, stopping SOAP server"
					os._exit(1)
			except:
				print "- "+str(sys.exc_info())
				print "Heartbeat failed, stopping SOAP server"
				os._exit(1)


############################## Utility functions for use in pipeline scripts

# utility function for scripts that are rendering views with arbitrary sizes
def MoveParallelCameraToImagePosition(camera, xPos, yPos, maxViewDimension):
	camPos = camera.GetPosition()
	camFP = camera.GetFocalPoint()
	xP = xPos+(maxViewDimension/2)
	yP = yPos-(maxViewDimension/2)
	camera.SetPosition(xP, yP, camPos[2])
	camera.SetFocalPoint(xP, yP, camFP[2])

# Use this chroma colour when the output images are not written with alpha channel: 
#ChromaColor = [0, 255, 0]
ChromaColor = [0, 0, 0]

def ReplaceChromaWithTransparency(inputFile, outputFile):
	import Image
	
	inImage = Image.open(inputFile)
	# add alpha layer which makes all pixels transparent
	inImage.putalpha(0);
	# get a pixel accessor	
	inImagePixels = inImage.load()
	for x in range(0, inImage.size[0]):
		for y in range(0, inImage.size[1]):
			aip = inImagePixels[x, y]
			if aip != (ChromaColor[0], ChromaColor[1], ChromaColor[2], 0):
				inImagePixels[x, y] = (aip[0], aip[1], aip[2], 255)
			else:
				inImagePixels[x, y] = (0, 0, 0, 0)

	inImage.save(outputFile)

class GeometryCluster:
	__clusterLines = []
	__clusterCenterCoordinateTotals = [0, 0]
	DistanceMeasure = 10
	
	def addLine(self, l):
		if len(self.__clusterLines) > 0 and self.__lineBelongsToCluster(l) == False:
			print "line does not belong to cluster: "+str(l)
			return False;

		self.__clusterLines.append(l)

		# update the cluster center
		self.__clusterCenterCoordinateTotals[0] = self.__clusterCenterCoordinateTotals[0] + ((l[2]-l[0])/2)
		self.__clusterCenterCoordinateTotals[1] = self.__clusterCenterCoordinateTotals[1] + ((l[3]-l[1])/2)
		print 'coordinate totals for cluster: '+str(self.__clusterCenterCoordinateTotals[0])+"/"+str(self.__clusterCenterCoordinateTotals[1])
		print 'cluster lines: '+str(len(self.__clusterLines))
		return True
		
		
	def __lineBelongsToCluster(self, l):
		lineCenter = [(l[2]-l[0])/2, (l[3]-l[1])/2]
		
		clusterCenter = self.getCenter()
		difference = [clusterCenter[0]-lineCenter[0], clusterCenter[1]-lineCenter[1]]
		
		if sqrt(difference[0]*difference[0]+difference[1]*difference[1]) < self.DistanceMeasure:
			return True
			
		return False 				 
	
	def getCenter(self):
		return [self.__clusterCenterCoordinateTotals[0] / len(self.__clusterLines), self.__clusterCenterCoordinateTotals[1] / len(self.__clusterLines)]
	

def ProcessAnnotationGeometry(annotationData):
	# analyze the annotation data (which should be vtkPolyData)
	
	geometryClusters = []

	print "annotation data cell count: "+str(annotationData.GetNumberOfCells())
	
	for i in range(0, annotationData.GetNumberOfCells()):
		tmpTuple1 = annotationData.GetCell(i).GetPoints().GetData().GetTuple3(0)
		tmpTuple2 = annotationData.GetCell(i).GetPoints().GetData().GetTuple3(1)
		tmpLine = [tmpTuple1[0], tmpTuple1[1], tmpTuple2[0], tmpTuple2[1]]
		
		c = 0
		lineAdded = False
		while c < len(geometryClusters) and geometryClusters[c].addLine(tmpLine) == False:
			c = c + 1

		if c == len(geometryClusters):
			print "Creating new cluster for line "+str(tmpLine)
			# create a new cluster
			gc = GeometryCluster()
			geometryClusters.append(gc)
			# add the line
			gc.addLine(tmpLine)
	
	print "Clusters: "+str(len(geometryClusters))
			
	for i in range(0, len(geometryClusters)):
		print geometryClusters[i].getCenter()
	
	
# this method will set up a list of annotation label text representations and add them to a target view
# the method will check which annotations are visible, and render labels for those annotations
# the annotation focalpoint, zoomfactor and view size will be used to define the visible rectangle of the annotation geometry
def GetAnnotationLabels(targetView, alignedModalitySources, alignedModalityLabels, annotationFocalPoint, zoomFactor, viewSize ):
	labelRepresentations = []

	# calculate the visible rectangle
	zoomedHalfViewDimension = [(viewSize[0]/2)/zoomFactor, (viewSize[1]/2)/zoomFactor]
	visibleRectangle = [annotationFocalPoint[0]-zoomedHalfViewDimension[0], annotationFocalPoint[1]-zoomedHalfViewDimension[1],annotationFocalPoint[0]+zoomedHalfViewDimension[0],annotationFocalPoint[1]+zoomedHalfViewDimension[1]] 

	for i in range(0, len(alignedModalitySources)):
		boundingBox = alignedModalitySources[i].GetDataInformation().DataInformation.GetBounds()

		# check if the annotation has a visible bounding box by comparing x1/x2 and y1/y2
		if boundingBox[0] != boundingBox[1] or boundingBox[2] != boundingBox[3]:
			# add labels
			labelVectorText = servermanager.sources.VectorText(Text=alignedModalityLabels[i])
			labelVectorRepr = servermanager.CreateRepresentation(labelVectorText, targetView)
			labelVectorRepr.Representation = 2 # outline = 3, points = 0, wireframe = 1, surface = 2, surface/w edges=5
				
			# define the scale
			scale = 10
			labelVectorRepr.Scale = [scale, scale, 1]
		
			# define the position
			labelVectorRepr.Position = [boundingBox[1]+5, boundingBox[3]-(1.5*scale), 0]
		
			tuple = (i, labelVectorRepr)
		
			labelRepresentations.append(tuple)
		
	return labelRepresentations
	


def SetRepresentationVisibility( alignedModalitySource_representationTuples, alignedModalityVisibilities):
	# set visibility on all aligned representations
	for i in range(0, len(alignedModalitySource_representationTuples)):
		# dissect the tuple
		alignedSourceNr = alignedModalitySource_representationTuples[i][0]
		alignedRepr = alignedModalitySource_representationTuples[i][1]
		
		# if the item should be visible, and it currently isn't: make it visible
		if alignedModalityVisibilities[alignedSourceNr] == True:
			if alignedRepr.Visibility.GetData() == 0:
				alignedRepr.Visibility = 1
		else:	# if the item should not be visible, and it currently is: make it invisible
			if alignedRepr.Visibility.GetData() == 1:
				alignedRepr.Visibility = 0

###################################


# the Paraview interface object for the gateway (all methods below are exposed to the gateway)
class ParaviewServerObject:
	__sessionId = -1				# the client session this python server is used for
	__connection = None				# the internal Paraview connection object
	__pm = None						# processmodule object for progress events
	__alg = None					# last seen processing Paraview algorithm
	__prog = None					# ...with its progress count
	
	transformModule = None					# Paraview does not expose the python module with Transforms itself
	alignedModalitySources = None			# List of aligned sources that have been prepared by the framework
	alignedModalityLabels = None			# ...along with their names
	alignedModalityVisibilities = None		# a list of booleans specifiying whether a given aligned source should be shown
	alignedModalityRepresentations = None 	# List of tuples of (sourceNr, representation) of aligned sources
											# (so there can be more than 1 representation per aligned source) 

	def __init__(self):
		if not servermanager.ActiveConnection:
		    self.__connection = servermanager.Connect()

		self.__pm = servermanager.vtkProcessModule.GetProcessModule()
		self.__pm.AddObserver("ProgressEvent", self.__progressObserver)

		# the module with transform proxies is not exposed yet
		self.transformModule = servermanager.createModule("transforms")

		self.alignedModalitySources = []
		self.alignedModalityLabels = []
		self.alignedModalityVisibilities = []
		self.alignedModalityRepresentations = []
		return

	def __progressObserver(self, caller, event):
		if(self.__pm.GetLastProgress() != self.__prog or
			self.__pm.GetLastProgressName() != self.__alg):

			self.__prog = self.__pm.GetLastProgress()
			self.__alg = self.__pm.GetLastProgressName()	

			# do a reverse soap call to the gateway
			try:
				proxy.setSessionProgress(self.__sessionId, self.__alg, self.__prog)
			except:
				# do nothing
				print ""
				# print "setJobProgress failed, is method available on the gateway?"
				
		return

	def __errorEventObserver(self, caller, event):
		print self
		print caller
		print event
		return

############################################################################################

	def ping(self):
		return "Pong"

	def quit(self):
		print "Closing on server request"
		os._exit(0)

	def setupSession(self, sessionId):
		self.__sessionId = sessionId
		return "+ Session registered: "+str(sessionId)

	def setupSource(self, sourceScript):
		try:
			#print pipelineScript
			# run the script within the scope of this class
			exec(sourceScript, globals(), self.__dict__)

			return "+ "+str(len(sourceScript))+" chars executed"
		except:
			print "setupSource exception in script: " + sourceScript		
			return "- Error ["+str(sys.exc_info()[1])+"] occurred on script line #"+str(sys.exc_info()[2].tb_lineno)+"\n"+str(sys.exc_info())

	
	def setupAlignedModalitySource(self, sourceScript, sourceLabel):
		try:
			# don't run the script in this class's scope, use a temporary dictionary to prevent overwriting the 'source'  
			# object containing the primary reader
			tempDictionary = {}
			
			exec(sourceScript, globals(), tempDictionary)
			
			# append the aligned reader to an array so we can reach it from visualization scripts
			self.alignedModalitySources.append(tempDictionary.get('source'))
			self.alignedModalityLabels.append(sourceLabel)
			self.alignedModalityRepresentations = []
			self.alignedModalityVisibilities.append(True)
	
			return "+ "+str(len(sourceScript))+" chars executed"
		except:
			print "setupAlignedModalitySource exception in script: " + sourceScript		
			return "- Error ["+str(sys.exc_info()[1])+"] occurred on script line #"+str(sys.exc_info()[2].tb_lineno)+"\n"+str(sys.exc_info())

	def setupPipeline(self, pipelineScript):
		try:
			#print pipelineScript
			# run the script within the scope of this class
			exec(pipelineScript, globals(), self.__dict__)

			return "+ "+str(len(pipelineScript))+" chars executed"
		except:
			print "setupPipeline exception in script: " + pipelineScript
			return "- Error ["+str(sys.exc_info()[1])+"] occurred on script line #"+str(sys.exc_info()[2].tb_lineno)+"\n"+str(sys.exc_info())
	
	def getParameters(self, parameterGetters):
		try:
			pvs = []
			for i in range(0, len(parameterGetters)):
				v = "?????"
				try:
					
					if len(str(parameterGetters[i])) > 0:
						vParam = eval(str(parameterGetters[i]), globals(), self.__dict__)
						v = str(vParam)
					
					# the '' around strings won't last after the soap serialization, force them around the string
					# if necessary 
					if isinstance(vParam, str):
						v = "'" + v + "'"
					
					#print "getter: "+str(parameterGetters[i])+" -> "+v
				except:
					print "Parameter getter "+str(parameterGetters[i])+" could not be evaluated"
				
				pvs.append(v)
				
			if len(pvs) == 0:
				return "- No parameters requested"

			return SOAPpy.arrayType(pvs)
		except:
			return "- Error ["+str(sys.exc_info()[1])+"] occurred on script line #"+str(sys.exc_info()[2].tb_lineno)+"\n"+str(sys.exc_info())
	
	def setParameters(self, parameterSetters, setterTypes, parameterValues):
		try:
			if len(parameterSetters) != len(setterTypes) or len(setterTypes) != len(parameterValues):
				return "- Sizes of input arrays need to be equal"	

			for i in range(0, len(parameterSetters)):
				if len(parameterSetters[i]) > 0:
					print "parameterSetter "+str(parameterSetters[i])+" -> "+str(parameterValues[i])
					if setterTypes[i].upper() == 'ASSIGNMENT':
						try:
							exec(str(parameterSetters[i])+" = "+str(parameterValues[i]), globals(), self.__dict__)
							
						except:
							return "- Assignment failed: "+str(sys.exc_info())
							
					elif setterTypes[i].upper() == 'FUNCTIONCALL':
						try:
							func = eval(parameterSetters[i], globals(), self.__dict__)
							if len(parameterValues[i]) > 0:
								value = eval(parameterValues[i], globals(), self.__dict__)
								func(value)
							else:
								func()
						except:
							return "- Setter failed:"+str(sys.exc_info())
					elif setterTypes[i].upper() == 'CUSTOMCODE':
						try:
							# ignore parameterValues
							exec(parameterSetters[i], globals(), self.__dict__)
						except:
							return "- Custom setter script failed: "+str(sys.exc_info())
					else:
						print "Setter type not supported: "+setterTypes[i]

			return "+ setParameters done"
		except:
			print "Something went wrong: "+str(sys.exc_info())
			return "- "+str(sys.exc_info())
	
	def runOutput(self, outputScript):
		try:
			# run the script within the scope of this class
			exec(outputScript, globals(), self.__dict__)
			
			return "+ "+str(len(outputScript))+" chars executed"
		except:
			print "runOutput exception in script: " + outputScript		
			return "- Error ["+str(sys.exc_info()[1])+"] occurred on script line #"+str(sys.exc_info()[2].tb_lineno)+"\n"+str(sys.exc_info())

# setup a reverse connection to the gateway/middleware, needed to update filter progress, etc.

gatewayHost = "localhost"
gatewayPort = "8080"
# gateway service should start with a slash, but NOT end with a slash
gatewayService = "/ViewerServer/services/Viewer"

for i in range(0, len(sys.argv)):
	if sys.argv[i].startswith("--gwHost="):
		gatewayHost = sys.argv[i].split("=")[1].strip()
	elif sys.argv[i].startswith("--gwPort="):
		gatewayPort = sys.argv[i].split("=")[1].strip()
	elif sys.argv[i].startswith("--gwService="):
		gatewayService = sys.argv[i].split("=")[1].strip()

# Setup SOAP client part: proxy to gateway. Also start the heartbeat thread (if the server fails to respond, this python script is terminated) 	
try:
	WSDLFile   = "http://" + gatewayHost + ":" + gatewayPort + "/" + gatewayService + "?wsdl"
	proxy      = WSDL.Proxy(WSDLFile)
	Heartbeat().start()
except:
	sys.exit("Could not create proxy to gateway, is the gateway running? (The error was "+str(sys.exc_info())+")")

# setup the SOAP server part of this script
server = SOAPpy.SOAPServer((socket.gethostname(), 0))

# notify the gateway where we can be reached
print "SOAP@"+str(server.server_address[0])+":"+str(server.server_address[1])

# warning: enabling following two lines creates a lot of output!
#server.config.dumpSOAPOut = 1
#server.config.dumpSOAPIn = 1

# register the paraview object with the SOAP server 
server.registerObject(ParaviewServerObject())

try:
	server.serve_forever()
except:
	pass
	
print "Server stopped";

